﻿// Copyrights (c) 2017 GlassGrass

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text;
using UnityEngine;
using UniRx;

using static UnityEngine.Debug;


namespace GlassGrass.Unity.GuiFramework
	{
	/// <summary> Binding object updation feature interface. </summary>
	public interface IBindingUpdater
		{

		/// <summary> Update view property. </summary>
		void UpdateViewByModel();

		/// <summary> Update model property. </summary>
		void UpdateModelByView();

		}



	/// <summary> Binding between model object property and view object property. </summary>
	/// <typeparam name="TValue"></typeparam>
	/// <typeparam name="TDisplay"></typeparam>
	public class Binding<TValue, TDisplay> : IBindingUpdater, IDisposable
		{

		private const BindingFlags PropertySeekFlags
			= BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public;



		private readonly Func<TValue> _getValue;

		private readonly Action<TValue> _setValue;

		private readonly Func<TDisplay> _getDisplay;

		private readonly Action<TDisplay> _setDisplay;

		private readonly IValueConverter<TValue, TDisplay> _converter;



		/// <summary> Be fired when this object was disposed. </summary>
		public IObservable<EventArgs> Disposed => _disposed;
		private readonly Subject<EventArgs> _disposed = new Subject<EventArgs>();



		/// <summary> Create Binding instance. </summary>
		/// <param name="modelObj"> [Non-Nullable] </param>
		/// <param name="modelProperty"></param>
		/// <param name="viewObj"> [Non-Nullable] </param>
		/// <param name="viewProperty"></param>
		/// <param name="converter">  </param>
		/// <exception cref="System.ArgumentNullException"></exception>
		/// <exception cref="System.ArgumentException">
		///     Type of modelObj does not have modelProperty property.
		///       or
		///     Type of viewObj does not have viewProperty property.
		/// </exception>
		public Binding(
			object modelObj, string modelProperty,
			IBindable<TDisplay> viewObj, string viewProperty,
			IValueConverter<TValue, TDisplay> converter)
			{
			if( modelObj == null )
				throw new ArgumentNullException( nameof( modelObj ) );

			if( viewObj == null )
				throw new ArgumentNullException( nameof( viewObj ) );

			var modelPInfo = modelObj.GetType().GetProperty( modelProperty, PropertySeekFlags )
				?? throw new ArgumentException( $"Type of modelObj does not have '{modelProperty}' property." );
			try
				{
				_getValue = GetGetAccessor<TValue>( modelObj, modelPInfo );
				_setValue = GetSetAccessor<TValue>( modelObj, modelPInfo );
				}
			catch( Exception e )
				{
				throw new ArgumentException( $"Type of modelObj does not have '{modelProperty}' property.", e );
				}

			var viewPInfo = viewObj.GetType().GetProperty( viewProperty, PropertySeekFlags )
				?? throw new ArgumentException( $"Type of viewObj does not have '{viewProperty}' property." );
			try
				{
				_getDisplay = GetGetAccessor<TDisplay>( viewObj, viewPInfo );
				_setDisplay = GetSetAccessor<TDisplay>( viewObj, viewPInfo );
				}
			catch( Exception e )
				{
				throw new ArgumentException( $"Type of viewObj does not have '{viewProperty}' property.", e );
				}

			_converter = converter ?? ValueConverter<TValue, TDisplay>.GetDefaultConverter();
			}



		/// <summary> Create Binding instance. </summary>
		/// <param name="getValue"> [Non-Nullable] </param>
		/// <param name="setValue"> [Non-Nullable] </param>
		/// <param name="getDisplay"> [Non-Nullable] </param>
		/// <param name="setDisplay"> [Non-Nullable] </param>
		/// <param name="converter"></param>
		/// <exception cref="System.ArgumentNullException"></exception>
		public Binding(
			Func<TValue> getValue, Action<TValue> setValue,
			Func<TDisplay> getDisplay, Action<TDisplay> setDisplay,
			IValueConverter<TValue, TDisplay> converter)
			{
			_getValue = getValue ?? throw new ArgumentNullException( nameof( getValue ) );
			_setValue = setValue ?? throw new ArgumentNullException( nameof( setValue ) );
			_getDisplay = getDisplay ?? throw new ArgumentNullException( nameof( getDisplay ) );
			_setDisplay = setDisplay ?? throw new ArgumentNullException( nameof( setDisplay ) );
			_converter = converter ?? ValueConverter<TValue, TDisplay>.GetDefaultConverter();
			}



		/// <summary> Update view property. </summary>
		public void UpdateViewByModel()
			{
			try
				{
				var newValue = _getValue();
				var oldValue = _converter.Parse( _getDisplay() ) ?? new Optional<TValue>();
				if( !oldValue.IsValidValue || !_converter.Equals( newValue, oldValue.Value ) )
					{
					_setDisplay( _converter.Format( newValue ) );
					}
				}
			catch( Exception e )
				{
				Log( e.ToString() );
				}
			}



		/// <summary> Update model property. </summary>
		public void UpdateModelByView()
			{
			try
				{
				var oldValue = _getValue();
				var newValue = _converter.Parse( _getDisplay() );

				// if invalid view display expression then this reverts display with old model value
				if( !newValue.IsValidValue )
					{
					_setDisplay( _converter.Format( oldValue ) );
					return;
					}

				if( !_converter.Equals( oldValue, newValue.Value ) )
					{
					_setValue( newValue.Value );
					}
				}
			catch( Exception e )
				{
				Log( e.ToString() );
				}
			}



		/// <summary> Dispose this binding. </summary>
		public void Dispose()
			{
			_disposed.OnNext( EventArgs.Empty );
			_disposed.OnCompleted();
			_disposed.Dispose();
			}



		private Func<T> GetGetAccessor<T>(object obj, PropertyInfo pInfo)
			=> Delegate.CreateDelegate( typeof( Func<T> ), obj, pInfo.GetGetMethod() ) as Func<T>;



		private Action<T> GetSetAccessor<T>(object obj, PropertyInfo pInfo)
			{
			var setter = pInfo.GetSetMethod();
			return setter != null
				? Delegate.CreateDelegate( typeof( Action<T> ), obj, setter ) as Action<T>
				: _ => { };
			}


		}

	}
